#!/usr/bin/perl
package TiedHash;
use strict;

sub new
{
    my $class = shift;
    $class = (ref $class || $class);
    my $self;
    tie %$self, $class, @_;
    bless $self, $class;
    return $self;
}

sub TIEHASH
{
    my $class = shift;
    return bless shift || {} => $class;
}

sub FIRSTKEY
{
    my $self = shift;
    my $a = keys %$self; # reset each() iterator
    return $self->NEXTKEY();
}

sub NEXTKEY
{
    my $self = shift;
    my $key;
    while (defined ($key = each %$self) and $key =~ m/^_/) {}
    return $key;
}

sub EXISTS
{
    my $self = shift;
    my $key = shift;
    return exists $self->{$key};
}

sub down_chain
{
    my $elt = shift; # (self)

    while (@_) {
        my $key = shift;
        exists $elt->{$key}
            || return undef;
        $elt = $elt->{$key};
        ref $elt eq 'HASH'
            || last;
    }
    if (@_) {
        return undef;
    }
    return $elt;
}

1;

package TiedHashDeletable;
use strict;
use base 'TiedHash';

sub DELETE
{
    my $self = shift;
    my $key = shift;
    delete $self->{$key};
}

1;

package Options;
use base 'TiedHash';
use strict;
use UNIVERSAL qw(isa);
use Carp;
use Getopt::Long qw(GetOptionsFromArray :config no_ignore_case);
use Cwd qw(abs_path);

Getopt::Long::Configure(qw(
    permute
    pass_through
));

my %DEFAULTS = (
    build_method => 'deb-pkg',
    share_dir => '/usr/local/share/kernel-package',
    trunk_url => 'git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-2.6-stable.git'
);

my %OPTIONS = (
    # Format:
    # name          => ['aliases', 'type', 'description', 'dashed_variant'(generated)]
    src_dir         => ['s', '=s', "Directory for Git working tree"],
    git_dir         => ['s', '=s', "Directory for Git repository tree"],
    build_dir       => ['',  '=s', "Directory where kernel is built"],
    share_dir       => ['',  '=s', "Defaults to $DEFAULTS{share_dir}"],
    branch_prefix   => ['p', '=s', "Prefix for branch names"],
    version         => ['V', '=s', "Git version to derive branch from: master, last_stable or version tag."],
    checkout        => ['',  '',  "Clone initial git repository from git.kernel.org"],
    # TODO: update-since feature
    trunk_url       => ['R', '=s', "Main repository URL to clone from and check tags"],
    update_trunk    => ['u', '!',  "Pull master branch from remote repository"],
    verbose         => ['v+', '',  "Verbose operation"],
    merge_branches  => ['',  '=s@{1,2}', "Merge with custom branch", []],
    remote_branches => ['', '=s@{2,3}', "Remote tracked branch", {}],
    update_branches => ['', '=s@{1,}', "Fetch remote tracked branch before merge"],
    kernel_config   => ['C', '=s', "Kernel config file"],
    track_config    => ['',  '!',  "Use branch to track kernel config"],
    config_mode     => ['m',  '=s', "Kernel config command; may be 'auto', 'skip' or any kernel config target (oldconfig, menuconfig, etc.)"],
    interactive     => ['',  '',  "Same as --config-mode=oldconfig"],
    build           => ['b',  '!',  "Build kernel package"],
    build_only      => ['B',  '!',  "Implies --build; skip branches updating and merging, just build"],
    build_method    => ['',  '=s', "Method of building package: 'deb-pkg' (default) or 'kpkg'"],
    package_only    => ['P',  '!',  "Implies --build; if --build-method=kpkg skip kernel building, just build .deb package"],
    jobs            => ['j', '=i', "Build kernel with parallel jobs"],
    make_flags      => ['',  '=s', "Build kernel custom make flags"],
    arch            => ['kernel-architecture', '=s', "Kernel architecture (as in ARCH variable)"],
    deb_arch        => ['debian-architecture', '=s', "Architecture of built .deb package (as in DEB_HOST_ARCH)"],
    config_file     => ['c', '=s', "Script configuration file"],
    all             => ['a', '!',  "Process all sections from configuration file"],
    dump_config     => ['D', '',  "Don't execute anything, dump configuration"],
    dump_all        => ['', '', "Don't execute anything, dump all data"]
);

use constant {
    ALIASES     => 0,
    TYPE        => 1,
    DESCRIPTION => 2,
    DEFAULT     => 3,
    DASHED      => 4
};

my %VALID_BUILD_METHODS = (
    'deb-pkg' => 1,
    'kpkg' => 1
);

my %DEBIAN_ARCHITECTURES = (
    x86_64 => 'amd64'
);

my @GETOPT_OPTS = map {
    my $specifier = $_;
    my $dashed = $_;
    my $aliases = $OPTIONS{$_}->[ALIASES];

    $specifier .= "|${dashed}"
        if $dashed =~ s/_/-/g;
    $specifier .= "|${aliases}"
        if $aliases;
    $specifier .= $OPTIONS{$_}->[TYPE];

    $OPTIONS{$_}->[DASHED] = $dashed;
    $specifier
} keys %OPTIONS;

sub FETCH
{
    my $self = shift;
    my $o = shift;
    if ($o =~ m/^(.+)!$/) {
        return $self->require($1);
    }
    return exists $self->{$o} ? $self->{$o} : $OPTIONS{$o}->[DEFAULT];
}

sub STORE
{
    my $self = shift;
    my $o = shift;
    my $val = defined $_[0] ? shift : 1;
    return $self->{$o} = $val;
}

sub from_array
{
    my $class = shift;
    my $array = shift;
    my $options = {};
    GetOptionsFromArray($array, $options, @GETOPT_OPTS);
    return $class->new($options);
}

sub from_argv
{
    my $class = shift;
    my $options = {};
    GetOptions($options, @GETOPT_OPTS);
    return $class->new($options);
}

sub put_override
{
    my $self = shift;
    my $override = shift || {};
    for my $o (keys %$override) {
        $self->{$o} = $override->{$o};
    }
}

sub put_defaults
{
    my $self = shift;
    my $defaults = shift || {};
    for my $o (keys %$defaults) {
        $self->{$o} = $defaults->{$o}
            unless exists $self->{$o};
    }
}

sub validate
{
    my $self = shift;
    my $section_name = shift;

    $self->put_defaults(\%DEFAULTS);

    if (!exists $self->{dump_config}) {
        die "Wrong value '$self->{build_method}' for --build-method"
            unless exists $VALID_BUILD_METHODS{$self->{build_method}};
    }

    $self->{build_only} = 1
        if exists $self->{package_only};
    $self->{build} = 1
        if exists $self->{build_only};

    if (! exists $self->{config_mode}) {
        if (exists $self->{build}) {
            $self->{config_mode} = 'auto';
        }
    }

    if (exists $self->{arch} && !exists $self->{deb_arch}
        && exists $DEBIAN_ARCHITECTURES{$self->{arch}})
    {
        $self->{deb_arch} = $DEBIAN_ARCHITECTURES{$self->{arch}};
    }

    if (!exists $self->{branch_prefix} && defined $section_name) {
        $self->{branch_prefix} = $section_name;
    }

    if (exists $self->{remote_branches}) {
        $self->{remote_branches} = parse_remote_branches($self->{remote_branches});
    }

    if (exists $self->{merge_branches}) {
        $self->{merge_branches} = $self->parse_merge_branches($self->{merge_branches});
    }

    if (exists $self->{update_branches}) {
        $self->{update_branches} = { map {$_ => 1} @{$self->{update_branches}} };
    }

    if (exists $self->{src_dir}) {
        $self->{src_dir} = abs_path($self->{src_dir});
        if (!exists $self->{git_dir}) {
            $self->{git_dir} = $self->{src_dir} . "/.git";
        }
    }
}

sub parse_remote_branches
{
    my $args = shift;
    my $elt;
    my $out = {};

    for (@$args) {
        m/^url=(.+)$/ && do {
            if (exists $elt->{url}) {
                die "Second URL ". $elt->{url}. " is not allowed in --remote-branch\n";
            }
            $elt->{url} = $1;
            next;
        };
        m/^branches=(.+)$/ && do {
            $elt->{branches} = [ split(',', $1) ];
            next;
        };
        $elt = { name => $_ };
        $out->{$_} = $elt;
    }
    return $out;
}

sub parse_merge_branches
{
    my $self = shift;
    my $args = shift;
    my $out = {};
    my $key;
    my $num = 0;

    for (@$args) {
        if ($num == 1) {
            if (m/^branch=(.+)$/) {
                $out->{$key} = $1;
                $num = 0;
                next;
            }
        }
        $key = $_;

        my $branches = $self->down_chain(remote_branches => $key => 'branches');
        $out->{$key} = defined $branches ? $branches->[0] : 'master';
        $num = 1;
    }

    return $out;
}

sub require
{
    my $self = shift;
    my $o = shift;
    my $message = shift;

    if (! exists $self->{$o}) {
        print STDERR $message, "\n"
            if defined $message;
        confess "Required option missed: --". $OPTIONS{$o}->[DASHED];
    }
    return $self->{$o};
}

1;


package Config;
use strict;
use Carp;
use Text::ParseWords;
use Data::Dumper;

sub parse
{
    my $filename = shift;
    my $result = {};
    open FILE, $filename;
    my $section = '_'; # global section name
    my $line = 0;
    while (<FILE>) {
        ++$line;
        # empty or comment lines
        m/^\s*(#.*)?$/ && next;
        # sections
        m/^\s*\[(.+)\][[:space:]#]*$/ && do {
            $section = $1;
            next;
        };
        # setting lines
        m/^\s*(\S.*[^[:space:]#])[[:space:]#]*$/ && do {
            push @{$result->{$section}}, shellwords("--$1");
            next;
        };
        die "Config parse failed ($filename:$line):\n$_\n";
    }
    return $result;
}

sub get_configuration
{
    my %result;
    my $cmdline_opts = Options->from_argv();

    $cmdline_opts->put_defaults({
        exists $ENV{KERNEL_BUILD_CONFIG} ?
            (config_file => $ENV{KERNEL_BUILD_CONFIG}) : ()
    });

    my $config_file = $cmdline_opts->{config_file};
    if (defined $config_file) {
        my $config = parse($config_file);
        my %process_sections;
        for (my $i = 0; $i < @ARGV;) {
            my $arg = $ARGV[$i];
            if (exists $config->{$arg}) {
                $process_sections{$arg} = 1;
                splice @ARGV, $i, 1;
                next;
            }
            ++$i;
        }

        if (@ARGV) {
            die 'Unknown arguments: ', join(', ', @ARGV), "\n";
        }

        my $common_opts = Options->from_array($config->{_});

        if (! keys %process_sections &&
            (defined $cmdline_opts->{all} || defined $common_opts->{all}))
        {
            %process_sections = map { $_ => 1 } grep { $_ ne '_' } keys %$config;
        }

        for my $s (keys %process_sections) {
            my $section_opts = Options->from_array($config->{$s});
            $section_opts->put_override($cmdline_opts);
            $section_opts->put_defaults($common_opts);
            $section_opts->validate($s);
            $result{$s} = $section_opts;
        }
        if (! keys %process_sections) {
            # no sections on command-line, so process only common config
            $common_opts->put_override($cmdline_opts);
            $common_opts->validate();
            $result{'_'} = $common_opts;
        }
    } else {
        $cmdline_opts->validate();
        $result{'_'} = $cmdline_opts;
    }
    # dump config
    if ($cmdline_opts->{dump_config}) {
        print Dumper(\%result);
        exit;
    }
    return \%result;
}

1;

package GitInfo;
use base 'TiedHashDeletable';
use strict;

use Carp;
use IPC::Open3;
use Symbol;

my %REGISTRY = (
    branches            => \&_get_branches,
    current_branch      => \&_get_branches,
    remote_branches     => \&_get_remote_branches,
    remote_urls         => \&_get_remote_urls,
    sorted_tags         => \&_get_sorted_tags,
    main_version        => \&_get_versions,
    kernel_version      => \&_get_versions,
    main_version_tag    => \&_get_versions,
    version_tag         => \&_get_versions,
    major               => \&_get_versions,
    patchlevel          => \&_get_versions,
    sublevel            => \&_get_versions,
    extraversion        => \&_get_versions,
    config_branch       => \&_get_config_branch,
    main_branch         => \&_get_main_branch,
    trunk_tags          => \&_get_trunk_tags,
#    _options            => \&_require_options
);

sub new
{
    my $class = shift;
    my $o = shift;
    my $self = TiedHash::new($class, {_options => $o});
    return $self;
}

sub FETCH
{
    my $self = shift;
    my $key = shift;

    confess "Wrong key '${key}'"
        unless exists $REGISTRY{$key};

    &{$REGISTRY{$key}}($self)
        unless exists $self->{$key};

    return $self->{$key};
}

# Public interface have blessed and tied $self

sub expand
{
    my $self = shift;
    for (keys %REGISTRY) {
        my $x = $self->{$_};
    }
}

# These methods have just blessed $self, not tied

sub _get_branches
{
    my $self = shift;

    $self->{branches} = {};

    for ($self->git('branch')) {
        m/^\*\s+(\S+)$/ && do {
            $self->{current_branch} = $1;
            $self->{branches}->{$1} = 1;
            next;
        };
        m/^\s*(\S+)$/ && do {
            $self->{branches}->{$1} = 1;
            next;
        };
        m/^\*\s+\(no branch\)^\s*$/ && do {
            next;
        };
        die "git branch parsing failed: $_\n";
    }
}

sub _get_remote_branches
{
    my $self = shift;
    $self->{remote_branches} = {};

    for ($self->git(qw'branch -r')) {
        m|^\s*([^/]+)/(.+)$| ||
            confess "Unexpectedly formed line: ". $_;

        my $remote = $1;
        my $rbranch = $2;
        $rbranch =~ m/^HEAD\s*->/ && next;
        push @{$self->{remote_branches}->{$remote}}, $rbranch;
    }
}

sub _get_remote_urls()
{
    my $self = shift;
    $self->{remote_urls} = {};
    for ($self->git(qw'remote -v')) {
        # $1 - remote name
        # $2 - URL
        # $3 - method (fetch|push)
        m/^\s*(\S+)\s+(\S+)\s+\((\w+)\)$/ ||
            confess "Unexpectedly formed line: ". $_;
        $self->{remote_urls}->{$1}->{$3} = $2;
    }
}


sub _get_sorted_tags
{
    my $self = shift;
    $self->{sorted_tags} = [sort {
        $a =~ /^v(.+)$/;
        my $a1 = $1;
        $b =~ /^v(.+)$/;
        my $b1 = $1;
        compare_versions($a1, $b1);
    } grep {
        m/^v\d+\.\d+\.\d+(\.\d+)?(-rc\d+)?$/
    } grep {
        exists $self->FETCH('trunk_tags')->{$_}
    } $self->git('tag')];
}

sub _get_trunk_tags
{
    my $self = shift;
    my $o = $self->{_options};
    my $tag;
    grep {
        m|\s+refs/tags/([^^]+)$|
            and $self->{trunk_tags}->{$1} = 1
    } $self->git('ls-remote', $o->{'trunk_url!'});
}

sub _get_versions
{
    my $self = shift;
    my $o = $self->{_options};
    my $version_tag = $o->{'version!'};

    if ($version_tag eq 'master') {
        $self->{version_tag} = 'master';
        $version_tag = $self->FETCH('sorted_tags')->[-1];
    } elsif ($version_tag eq 'last_stable') {
        $version_tag = (
            grep {m/^v\d+\.\d+\.\d+(\.\d+)?$/}
            @{$self->FETCH('sorted_tags')}
        )[-1];
        $self->{version_tag} = $version_tag;
    }
    $version_tag =~ m/^v(((\d+)\.(\d+)\.(\d+))(\..+)?)$/;
    # used for naming branches
    $self->{main_version} = $2;
    # used in templates
    $self->{major} = $3;
    $self->{patchlevel} = $4;
    $self->{sublevel} = $5;
    $self->{extraversion} = $6;
    # used for comparing with config version
    $self->{kernel_version} = $1;
    # used for creation of branches
    $self->{main_version_tag} = "v$2";
    # TODO: check that $self->{main_version_tag} exists in git tags
}

sub update_versions
{
    my $self = shift;
    delete $self->{sorted_tags};
    delete $self->{main_version};
    my $x = $self->{main_version};
}

sub _get_config_branch()
{
    my $self = shift;
    my $o = $self->{_options};
    $self->{config_branch} = $o->{'branch_prefix!'} . "-config";
}

sub _get_main_branch()
{
    my $self = shift;
    my $o = $self->{_options};
    $self->{main_branch} = $o->{'branch_prefix!'}. "-". $self->FETCH('main_version');
}

sub git
{
    my $self = shift;
    my $o = $self->{_options};
    my ($in, $out);
    my (@out, @err);
    my $err = Symbol::gensym;
    unshift @_, "--git-dir=$o->{'git_dir!'}";
    my $pid = IPC::Open3::open3($in, $out, $err, git => @_);
    close $in;
    chomp(@out = <$out>);
    @err = <$err>;
    waitpid $pid, 0;
    if ($?) {
        print STDERR @err;
        die "Git command failed: git ", join(' ', @_), "\n";
    }
    return @out;
}

sub compare_versions
{
    my @a = break_version(shift);
    my @b = break_version(shift);

    for (my $i = 0; $i < 5; ++$i) {
        my $res = $a[$i] <=> $b[$i];
        return $res
            if $res;
    }
    return 0;
}

sub break_version
{
    $_[0] =~ /^(\d+)\.(\d+)\.(\d+)(\.(\d+))?(-rc(\d+))?$/
        or confess "Unknown version format: $_[0]";
    return ($1, $2, $3, defined $5 ? $5 : 0, defined $7 ? $7 : 999);
}

#sub _require_options
#{
#    my $self = shift;
#    confess "Undefined _options"
#        unless exists $self->{_options};
#}

1;

package KernelBuild;
use strict;

use IPC::Open3;
use Symbol;
use Cwd qw(abs_path getcwd);
use Carp;
use File::Copy;
use Data::Dumper;

sub execute
{
    my $class = shift;
    $class = (ref $class || $class);
    my $o = shift;
    my $self = bless {
        options => $o,
        info =>  GitInfo->new($o)
    } => $class;
    $self->execute_main;
}

sub get_system_status
{
    my $rc = shift;
    if ($rc == -1) {
        confess "Command failed: ", join(' ', @_), "\n";
    }

    if ($rc & 127) {
        confess "Command died with signal " . ($rc & 127) .
            (($rc & 128) ? ' (core dumped): ' : ': ') .
            join(' ', @_), "\n";
    }
    return $rc >> 8;
}

sub check_system_rc
{
    my $status = get_system_status($_[0]);

    return 1
        if $status == 0;

    die "Failed with status $status\n";
}

sub shell
{
    my $self = shift;
    $self->verbose_print2('Executing: ', join(" ", @_));
    check_system_rc(system @_);
}

sub check_git_repository
{
    my $git_dir = shift;
    -d $git_dir || return 0;
    -d "${git_dir}/.git" && return 1;
    die "No git repository at ${git_dir}";
}

sub execute_main
{
    my $self = shift;
    my $o = $self->{options};
    my $i = $self->{info};

    my $src_dir = $o->{'src_dir!'};

    if ($o->{dump_all}) {
        $i->expand();
        print Dumper($self);
        exit;
    }

    if (!check_git_repository $src_dir) {
        $o->require('checkout', "No source dir ${src_dir}");
        $self->verbose_print("Performing checkout...");
        $self->shell("git clone $o->{'trunk_url!'} ${src_dir}");
    } elsif (! -f "${src_dir}/include/linux/Kbuild") {
        die "No kernel sources at ${src_dir}";
    }

    my $work_dir = getcwd;
    my $kernel_config = $o->{kernel_config};

    if (defined $kernel_config) {
        if (!-f $kernel_config) {
            die "Config file '${kernel_config}' not exists!\n"
        }
        $o->{kernel_config} = abs_path($kernel_config);
    }

    croak_failed(chdir $src_dir);

    if (!$o->{build_only} && $o->{update_trunk}) {
        $self->verbose_print("Fetching trunk...");
        $self->shell("git fetch --quiet origin");
        $i->update_versions();
    }

    # TODO: last_rc feature
    if ($o->{version} eq 'last_stable') {
        $self->verbose_print("Last stable version: ", $i->{version_tag});
    } elsif ($o->{version} eq 'master') {
        $self->verbose_print('Using master branch (' . $i->{sorted_tags}->[-1] . ')');
    }

    if (!$o->{build_only}) {
        if ($o->{track_config}) {
            $self->prepare_config_branch();
        }

        $self->prepare_main_branch();
    }

    if ($o->{config_mode}) {
        if (!$o->{build_dir}) {
            $o->{build_dir} = "${work_dir}/build-" . $o->{'branch_prefix!'};
        }
        $self->configure_and_build();
    }
}

############ Function declarations ##############

sub verbose_print
{
    my $self = shift;
    my $o = $self->{options};
    if ($o->{verbose}) {
        print '[kernel-build] ', @_, "\n";
    }
}

sub verbose_print2
{
    my $self = shift;
    my $o = $self->{options};
    if ($o->{verbose} > 1) {
        $self->verbose_print(@_);
    }
}

sub croak_failed
{
    if (!$_[0]) {
        confess("Call failed");
    }
    return @_;
}

sub get_config_version
{
    my $file = shift;
    open FILE, $file
        or confess "$file: $!";

    while (<FILE>) {
        m/^# Linux kernel version: (\d+\.\d+\.\d+)(\..+)?$/
            && return $1 . $2;
    }
    die "Not found version in config $file!\n";
}

sub prepare_config_branch
{
    my $self = shift;
    my $o = $self->{options};
    my $i = $self->{info};

    my $kernel_config;
    if (!check_git_repository $o->{config_dir}) {
    #if (! exists $i->{branches}->{$i->{config_branch}}) {
        # create git repository
        $kernel_config = $o->require('kernel_config',
            "Config branch '$i->{config_branch}' not found and no option --kernel-config supplied!");
        $self->create_branch($i->{config_branch}, $i->{main_version_tag});
        $self->verbose_print("Adding config from ${kernel_config}...");
        croak_failed(copy(${kernel_config}, "config"));
        $self->shell('git add config');
        $self->shell('git commit -q -m "Initial config"');
    } elsif ($kernel_config = $o->{kernel_config}) {
        $self->switch_branch($i->{config_branch});
        croak_failed(copy($kernel_config, "config"));
        $self->shell('git commit -q -m "Modified config" config');
    }
}

sub prepare_main_branch
{
    my $self = shift;
    my $o = $self->{options};
    my $i = $self->{info};

    # TODO: any merges in $i->{main_branch} should be reverted if something fails

    if (! exists $i->{branches}->{$i->{main_branch}}) {
        $self->create_branch($i->{main_branch}, $i->{version_tag});
    } else {
        $self->switch_branch($i->{main_branch});
        $self->verbose_print("Merging with $i->{version_tag}...");
        $self->shell("git merge --no-stat --no-log $i->{version_tag}");
    }

    if ($o->{track_config}) {
        $self->verbose_print("Merging with $i->{config_branch}...");
        # FIXME: discrepancy here
        $self->shell("git merge --no-stat --no-log $i->{config_branch}");
    }

    for (keys %{$o->{merge_branches}}) {
        my $branch_merge = $_;
        if (! exists $i->{branches}->{$_}) {
            $branch_merge = "${_}-$i->{main_version}";
            if (!exists $i->{branches}->{$branch_merge}) {
                if (exists $o->{remote_branches}->{$_}) {
                    $self->prepare_remote_branch($o->{remote_branches}->{$_});
                    my $remote_branch = $self->expand_template($o->{merge_branches}->{$_});

                    $self->shell("git branch --track ${branch_merge} ${_}/${remote_branch}");
                    delete $i->{branches};
                } else {
                    die "You asked to merge branch '$_', but no such branch exists nor configured as remote!\n";
                }
            }
        }
        if ($o->down_chain(update_branches => $_)) {
            $self->verbose_print("Pulling from '${branch_merge}'...");
            $self->shell("git pull --no-stat --no-log --quiet . ${branch_merge}");
        } else {
            $self->verbose_print("Merging with '${branch_merge}'...");
            $self->shell("git merge --no-stat --no-log ${branch_merge}");
        }
    } # for (merge_branches)
}

sub prepare_remote_branch
{
    my $self = shift;
    my $remote = shift;
    my $i = $self->{info};
    my $o = $self->{options};
    my $remote_name = $remote->{name};
    my $remote_url = $self->expand_template($remote->{url});

    if (!exists $i->{remote_urls}->{$remote_name}) {
        my $branches = join(" ", map("-t ". $self->expand_template($_), @{$remote->{branches}}));
        $self->shell("git remote add ${branches} ${remote_name} ${remote_url}");
        delete $i->{remote_urls};
    } elsif ($i->{remote_urls}->{$remote_name}->{fetch} ne $remote_url) {
        die "Remote URL mismatch, please delete remote '${remote_name}' manually: git remote rm ${remote_name}\n";
    }

    if (!exists $i->{remote_branches}->{$remote_name}) {
        $self->verbose_print("Fetching remote '${remote_name}'...");
        $self->shell("git fetch --quiet ${remote_name}");
        delete $i->{remote_branches};
    }


}

sub expand_template
{
    my $self = shift;
    my $i = $self->{info};
    my $template = shift;
    while ($template =~ m/(%(.+)%)/) {
        my $replace = $1;
        my $with = $i->{$2};
        $template =~ s/$replace/$with/g;
    }
    return $template;
}



use File::stat;

sub configure_and_build
{
    my $self = shift;
    my $o = $self->{options};
    my $i = $self->{info};
    my $build_dir = $o->{'build_dir!'};

    if (!-d $build_dir) {
        croak_failed(mkdir $build_dir);
    }

    my $kernel_config = $o->{kernel_config};
    my $tracked_config =
        $o->{track_config} ?
            'config' :
            $kernel_config ? $kernel_config : undef;
    my $build_config = "${build_dir}/.config";

    if ($tracked_config) {
        croak_failed(copy($tracked_config, $build_config));
    }

    my $config_mode = $o->{'config_mode!'};

    my @make_opts = ("O=${build_dir}");
    my @kpkg_opts = ("BUILD_DIR=${build_dir}");

    if ($o->{jobs}) {
        push @make_opts, (-j => $o->{jobs});
    }
    if ($o->{make_flags}) {
        push @make_opts, split(' ', $o->{make_flags});
    }
    my $arch;
    if ($arch = $o->{arch}) {
        push @make_opts, "ARCH=${arch}";
        push @kpkg_opts, "ARCH=${arch}";
        if (!$o->{deb_arch}) {
            print STDERR "Warning: deb_arch not found for ${arch}, shoud specify --debian-architecture\n";
        } else {
            $ENV{DEB_HOST_ARCH} = $o->{deb_arch};
        }
    }

    $self->switch_branch($i->{main_branch});
    if ($config_mode eq 'auto') {
        # TODO: error descriptive message if no $tracked_config || $build_config exists
        my $config_version = get_config_version($tracked_config || $build_config);
        if ($config_version eq $i->{kernel_version}) {
            $self->verbose_print("Kernel and config versions match $config_version");
            $config_mode = $o->set('config_mode', 'skip');
        } elsif (GitInfo::compare_versions($i->{kernel_version}, $config_version) > 0) {
            $self->verbose_print("Upgrading config from $config_version to $i->{kernel_version}...");
            $config_mode = $o->set('config_mode',
                $o->{interactive} ? 'silentoldconfig' : '__quiet_upgrade');
        } else {
            $self->verbose_print("Downgrading config from $config_version to $i->{kernel_version}...");
            $config_mode = $o->set('config_mode', 'silentoldconfig');
        }
    }

    # TODO: configure only when we got something from updating branches
    #       or --force-config option supplied
    #       or --config-mode=menuconfig
    if ($config_mode ne 'skip') {
        $self->verbose_print("Configuring kernel...");

        my $config_old = "${build_config}.old";
        my $old_st = stat($build_config);

        if ($config_mode eq '__quiet_upgrade') {
            quiet_upgrade(\@make_opts);
        } else {
            $self->shell(make => @make_opts, $config_mode);
        }

        my $new_st = stat($build_config);
        my $config_changed = 0;

        # detect if config has changed
        if ($old_st && $new_st && $old_st->mtime < $new_st->mtime) {
            # actual write was done, detect that it was not just comment change
            if (-f $config_old) {
                system('diff', '-dqBI', '^#', $config_old, $build_config);
                if (get_system_status($?) > 0) {
                    $config_changed = 1;
                } else {
                    $self->verbose_print(".config was not changed");
                }
            } else {
                print STDERR "Warning: .config.old was not created!\n";
                $config_changed = 1;
            }
        } elsif (!$old_st && $new_st) {
            $self->verbose_print("New .config was created");
            $config_changed = 1;
        }

        if ($config_changed) {
            if ($o->{track_config}) {
                $self->require_config_branch();
                $self->switch_branch($i->{config_branch});
                $self->verbose_print("Updating kernel config...");
                croak_failed(copy($build_config, $tracked_config));
                $self->shell('git commit -q -m "Updated config" config');
                $self->verbose_print("Saved config to git branch $i->{config_branch}");
                $self->switch_branch($i->{main_branch});
                $self->verbose_print("Merging with $i->{config_branch}...");
                $self->shell("git merge --no-stat --no-log $i->{config_branch}");
            } elsif ($kernel_config) {
                croak_failed(copy($build_config, $tracked_config));
                $self->verbose_print("Saved config to file " . $tracked_config);
            }
        }
    }
    # FIXME: kernel rebuilds always
    if ($o->{build}) {
        $self->verbose_print("Building in ${build_dir}...");
        if ($o->{build_method} eq 'deb-pkg') {
            $self->shell(make => @make_opts, 'deb-pkg');
        } else {
            if (!$o->{package_only}) {
                $self->shell(make => @make_opts, 'bzImage', 'modules');
            }
            $self->shell(make => '-f', $o->{share_dir} . "/kpkg_build.mk",
                    @kpkg_opts, 'kpkg_package');
        }
    }
}

my $sigpipe = 0;

sub sigpipe
{
    $sigpipe = 1;
}

BEGIN
{
    $SIG{PIPE} = \&sigpipe;
}

use IO::Select;
use IO::Handle;
use POSIX ":sys_wait_h";
use Fcntl;

sub quiet_upgrade
{
    my $make_opts = shift;
    my ($in, $out, $err);
    $err = gensym;
    # TODO: sometimes cleanup of build directory is needed
    # it is related to unsatisfactory timestamps due to git branches checkout
    my $pid = IPC::Open3::open3($in, $out, $err, make => @$make_opts, 'oldconfig');
    my $flags = fcntl($out, F_GETFL, 0);
    fcntl($out, F_SETFL, $flags | O_NONBLOCK);
    $flags = fcntl($err, F_GETFL, 0);
    fcntl($err, F_SETFL, $flags | O_NONBLOCK);
    my $r = IO::Select->new($out, $err);
    my $w = IO::Select->new($in);
    autoflush STDOUT 1;
    autoflush STDERR 1;
    my $waitpid;
    my $output_stage = 0; # 0 - before output; 1 - in output; 2 - output done
    do {
        my ($rset, $wset, $eset) = IO::Select->select($r, $w, undef, 0);
        for my $fd (@$rset) {
            my $print_fd = $fd == $out ? \*STDOUT : \*STDERR;
            my $buf;
            while (0 < sysread $fd, $buf, 1024) {
                print $print_fd $buf;
                if ($fd == $out) {
                    $output_stage = 1;
                }
            }
        }
        if (! @$rset && $output_stage == 1) {
            $output_stage = 2;
        }
        if ($output_stage == 2 && @$wset) {
            my $buf = "\n";
            print $buf;
            syswrite $in, $buf;
            $output_stage = 0;
        }
    } while (0 == ($waitpid = waitpid $pid, WNOHANG) && !$sigpipe);

    if ($waitpid > 0) {
        check_system_rc($?);
    } else {
        if ($sigpipe) {
            print STDERR "Got SIGPIPE!\n";
        }
        confess $!;
    }
}

sub switch_branch
{
    my $self = shift;
    my $i = $self->{info};

    my $branch = shift;
    if (not exists $i->{branches}->{$branch}) {
        confess "Not found branch '$branch' to switch!\n";
    }

    if ($i->{current_branch} ne $branch) {
        $self->verbose_print("Switching to branch $branch...");
        $self->shell("git checkout $branch");
        delete $i->{current_branch};
    }
}

sub create_branch
{
    my $self = shift;
    my $i = shift;

    my $branch_name = shift;
    my $version_tag = shift;
    if (!defined $branch_name || !defined $version_tag) {
        confess "Empty parameter: \$branch_name = '$branch_name', $version_tag = '$version_tag'";
    }
    $self->verbose_print("Creating branch $branch_name from $version_tag...");
    $self->shell("git checkout -b $branch_name $version_tag");
    delete $i->{branches};
    delete $i->{current_branch};
}

1;

package main;
use strict;

my $config = Config::get_configuration();

for my $s (keys %$config) {
    KernelBuild->execute($config->{$s});
}
